﻿using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;

namespace TorBrowserUpdater
{
    internal class TBBUpdater
    {

        //At the beginning I was parsing the version number via my regular expression,
        //then I took the version string directly from the source (versions.mk)
        //This way we can almost entierely recreate the filename and check if it exists in /dist/tor-browser.
        //But finally I ended up directly using the Download button link on the download page (download-easy.html).
        //https://gitweb.torproject.org/torbrowser.git/blob/HEAD:/build-scripts/versions.mk

        //"tor-browser-gnu-linux-i686-\d+\.\d+\.\d+-\d+\.\d+-dev-fr.tar.gz"

        /// <summary>
        /// Regular expression representing the name of the archive to download.
        /// Contains composite format arguments (must be formated with String.Format).
        /// This string is used for the Windows and the Linux version.
        /// Format args needed :
        /// -0 : The platform identifier ('gnu-linux' for linux, '' for Windows)
        /// -1 : The processor type (i686/i386 for x86, win32_64 for x64)
        /// -2 : The language code (two-letter language identifier, i.e. 'fr', 'en', 'it')
        /// -3 : The expected archive extension, depending if it's for linux or windows (tar.gz, exe, zip)
        /// </summary>
        private const string BASE_FILENAME_LINUX_AND_WIN =
            @"tor-browser-{0}{1}\d+\.\d+\.\d+-\d+(\.\d)*([-_]dev)?[-_]{2}{3}";

        /// <summary>
        /// Regular expression representing the name of the archive to download.
        /// Contains composite format arguments (must be formated with String.Format).
        /// This string is used for the Windows and the Linux version.
        /// Format args needed :
        /// -0 : The language code (two-letter language identifier, i.e. 'fr', 'en', 'it')
        /// -1 : The expected archive extension, depending if it's for linux or windows (tar.gz, exe, zip)
        /// </summary>
        private const string BASE_FILENAME_PLUGGABLE_TRANSPORT_WIN =
            @"tor-pluggable-transports-browser-\d+\.\d+\.\d+-(beta-|rc-)?\d+(\.\d)*(-pt\d+)?([-_]dev)?[-_]{0}{1}";

        /// <summary>
        /// Regular expression representing the name of the archive to download.
        /// Contains composite format arguments (must be formated with String.Format).
        /// This string is used for the Windows and the Linux version.
        /// Format args needed :
        /// -0 : The processor type (i686 for x86, win32_64 for x64)
        /// -1 : The language code (two-letter language identifier, i.e. 'fr', 'en', 'it')
        /// -2 : The expected archive extension, depending if it's for linux or windows (tar.gz, exe, zip)
        /// </summary>
        private const string BASE_FILENAME_PLUGGABLE_TRANSPORT_LINUX =
            @"tor-pluggable-transports-browser-gnu-linux-{0}-\d+\.\d+\.\d+-(beta-|rc-)?\d+(\.\d)*(-pt\d+)?([-_]dev)?[-_]{1}{2}";

        /// <summary>
        /// Mac Os hasn't been tested.  It's only here for future purpose.
        /// </summary>
        private const string BASE_FILENAME_OSX = @"TorBrowser-\d+\.\d+\.\d+-\d+(\.\d)?([-_]dev)?{0}{1}[-_]{2}{3}";

        private static readonly string[] DownloadMirrorsList =  { "https://www.torproject.org" };

        private string _language;
        private PlatformType _platform;
        private bool _is64Bit;

        public enum PlatformType
        {
            Windows,
            Linux,
            MacOs
        }

        public static bool NewVersionDownloaded { get; private set; }
        public static string DownloadedFilename { get; private set; }

        public static bool ErrorOccurred { get; private set; }
        public static Exception Error { get; private set; }

        public TBBUpdater(string language)
            : this(language, false)
        { }

        public TBBUpdater(string language, bool always32Bit)
            : this(language, always32Bit, GetPlatform())
        { }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="language">The language identifier (usually a two-letter string)</param>
        /// <param name="always32Bit">Wheter to force or not 32 bit version</param>
        /// <param name="platformType"></param>
        public TBBUpdater(string language, bool always32Bit, PlatformType platformType)
        {
            _language = language;
            _platform = platformType;
            _is64Bit = !always32Bit && Environment.Is64BitOperatingSystem;
        }

        public static PlatformType GetPlatform()
        {
            switch (Environment.OSVersion.Platform)
            {
                case PlatformID.Win32NT:
                    return PlatformType.Windows;
                case PlatformID.Unix:
                    return PlatformType.Linux;
                case PlatformID.MacOSX:
                    return PlatformType.MacOs;
                default:
                    return PlatformType.Windows;
            }
        }

        public void DownloadReleaseIfNewerExists()
        {
            try
            {
                string fullDownloadLink = RetrieveDownloadUrl();
                var filename = Path.GetFileName(new Uri(fullDownloadLink).LocalPath) ?? "";

                //A new version is available if the setup file of the newer version available online doesn't exists in the executable folder.
                //We get the installed version by parsing the "./Tor Browser/Docs/changelog" file.
                var versionParser = new VersionParser(_language);
                var currentVersion = versionParser.ParseCurrentVersion();
                var serverVersion = versionParser.ParseStringContainingVersion(filename);
                bool isServerVersionNewer = currentVersion == null || TBBVersionComparer.IsVersionNewer(serverVersion, currentVersion);
                bool localDirectoryExists = Directory.Exists(PathHelper.GetUncompressedTBBFolderPath(_language));
                if (!isServerVersionNewer && localDirectoryExists)
                {
                    NewVersionDownloaded = false;
                    DownloadedFilename = "";
                }
                else
                {
                    var signatureFilename = "";
                    if (!StartupOptions.SkipSignatureVerification)
                    {
                        SignatureVerifier.DownloadSignatureFile(fullDownloadLink, out signatureFilename);
                    }

                    Console.Write("Downloading setup file {0}...", filename);
                    Tools.DownloadFile(fullDownloadLink, filename);

                    if (!StartupOptions.SkipSignatureVerification)
                    {
                        var publicKey = GetPublicKey();
                        using (var sigStream = File.OpenRead(signatureFilename))
                        using (var pubKeyStream = Tools.ToStream(publicKey))
                        {
                            var sigPassed = SignatureVerifier.VerifySignature(filename, sigStream, pubKeyStream);
                            if (!sigPassed)
                            {
                                File.Delete(filename);

                                if (!StartupOptions.KeepSignatureFile)
                                {
                                    File.Delete(signatureFilename);
                                }

                                throw new TBBUpdaterException(
                                    "Setup signature verification FAILED ! the file is either corrupt or compiled by a bad guy !");
                            }
                        }
                    }

                    NewVersionDownloaded = true;
                    DownloadedFilename = filename;
                }

                ErrorOccurred = false;
                Error = null;
            }
            catch (Exception ex)
            {
                ErrorOccurred = true;
                Error = ex;
            }
        }

        /// <summary>
        /// Selects and returns the right public key for current options, reading it from the file specified by the user
        /// or, if no public key was specified, from resources.  The public key is different whether the pluggable transports
        /// option is enabled or not.
        /// </summary>
        /// <returns></returns>
        private string GetPublicKey()
        {
            if(StartupOptions.PublicKeyFile != null)
            {
                return File.ReadAllText(StartupOptions.PublicKeyFile);
            }

            return StartupOptions.UsePluggableTransports ? Properties.Resources.DavidFifieldPublicKey : Properties.Resources.ErinnClarkPublicKey;
        }

        private string RetrieveDownloadUrl()
        {
            //Regular expression representing the filename we're looking for
            var setupFilenamePattern = BuildDownloadUrlPattern();
            var mirrorUrl = DownloadMirrorsList.First();
            string downloadPage;
            if(StartupOptions.UsePluggableTransports)
            {
                //The pluggable version isn't available on the download-easy page, so we have to get it from the dist page (all downloads page).
                downloadPage = mirrorUrl.TrimEnd('/') + "/dist/torbrowser";
                if (_platform == PlatformType.Linux)
                    downloadPage += "/linux/";
            }
            else
            {
                downloadPage = mirrorUrl.TrimEnd('/') + "/download/download-easy.html.en";
            }

            var doc = Tools.DownloadHtmlDocument(downloadPage);
            var setupDownloadLinkNode = doc.DocumentNode.Descendants("a").
                FirstOrDefault(
                    n => Regex.IsMatch(n.GetAttributeValue("href", ""), setupFilenamePattern));

            if (setupDownloadLinkNode == null)
            {
                throw new TBBUpdaterException("The download link could not be found.");
            }

            string relativeDownloadLink = setupDownloadLinkNode.GetAttributeValue("href", "").Replace("en-US", _language);
            string fullDownloadLink;
            if(StartupOptions.UsePluggableTransports)
            {
                var baseUrl = mirrorUrl + "/dist/torbrowser/";
                if (_platform == PlatformType.Linux)
                    baseUrl += "linux/";
                fullDownloadLink = baseUrl + relativeDownloadLink;
            }
            else
            {
                fullDownloadLink = mirrorUrl + "/download/" + relativeDownloadLink;                
            }
            return fullDownloadLink;
        }

        private string BuildDownloadUrlPattern()
        {
            if(StartupOptions.UsePluggableTransports)
            {
                return BuildDownloadUrlPatternForPluggableTransports();
            }
            else
            {
                return BuildStandardDownloadUrlPattern();
            }
        }

        private string BuildStandardDownloadUrlPattern()
        {
            const string referenceLanguageIdentifier = "en-US";
            string archiveExtension;
            string filenamePattern;
            switch (_platform)
            {
                case PlatformType.Windows:
                    archiveExtension = ".exe";
                    filenamePattern = string.Format(
                        "torbrowser-install-{0}_{1}{2}",
                        VersionParser.VersionRegex,
                        referenceLanguageIdentifier,
                        Regex.Escape(archiveExtension));
                    break;

                case PlatformType.Linux:
                    var procArchitecture = Regex.Escape(_is64Bit ? "64" : "32");
                    archiveExtension = @"\.tar\.(gz|xz)";
                    filenamePattern = string.Format(
                        "tor-browser-linux{0}-{1}_{2}{3}",
                        procArchitecture,
                        VersionParser.VersionRegex + "(-dev)?",
                        referenceLanguageIdentifier,
                        archiveExtension);
                    break;
                default:
                    throw new InvalidOperationException("Invalid platform");
            }
            return filenamePattern;
        }

        private string BuildDownloadUrlPatternForPluggableTransports()
        {
            string regExpr;

            switch (_platform)
            {
                case PlatformType.Windows:
                    regExpr = String.Format(
                        BASE_FILENAME_PLUGGABLE_TRANSPORT_WIN,
                        _language,
                        ".exe");
                    break;

                case PlatformType.Linux:
                    regExpr = String.Format(
                        BASE_FILENAME_PLUGGABLE_TRANSPORT_LINUX,
                        _is64Bit ? "x86_64" : "i686",
                        _language,
                        ".tar.gz");
                    break;

                default:
                    regExpr = null;
                    break;
            }
            return regExpr;
        }

    }
}
